package com.ve.downloader;

import android.annotation.SuppressLint;

import com.ve.downloader.utils.FUtils;
import com.ve.downloader.utils.HexUtils;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;

import static com.ve.downloader.Utils.between;
import static com.ve.downloader.Utils.in;

public class Programmer {
    public static final String SERIAL_PARITY_EVEN = "SERIAL_PARITY_EVEN";
    public static final String SERIAL_PARITY_NONE = "SERIAL_PARITY_NONE";

    public static final String PROTOCOL_89 = "89";
    public static final String PROTOCOL_12C5A = "12c5a";
    public static final String PROTOCOL_12C52 = "12c52";
    public static final String PROTOCOL_12Cx052 = "12cx052";

    public static final String[] PROTOSET_89 = {PROTOCOL_89};
    public static final String[] PROTOSET_12 = {PROTOCOL_12C5A, PROTOCOL_12C52, PROTOCOL_12Cx052};
    public static final String[] PROTOSET_12B = {PROTOCOL_12C52, PROTOCOL_12Cx052};
    public static final String[] PROTOSET_PARITY = {PROTOCOL_12C5A, PROTOCOL_12C52};

    private int chkmode;
    private String protocol;
    private SerialInterface conn;
    private float fosc;
    private byte[] info;
    private String version;
    private byte[] model;
    private String name;
    private int romsize;

    private ByteBuffer mByteBuffer;
    boolean bluetoothMode = false;

    private ProgramListener programListener = info -> {
    };

    public void setProgramListener(ProgramListener programListener) {
        this.programListener = programListener;
    }

    /*************************/
    protected ByteBuffer getByteBuffer() {
        if (mByteBuffer == null) {
            mByteBuffer = ByteBuffer.allocate(1024);
        }
        mByteBuffer.rewind();
        return mByteBuffer;
    }

    public void setBluetoothMode(boolean bluetoothMode) {
        this.bluetoothMode = bluetoothMode;
    }

    /*****************/
    public Programmer(SerialInterface conn) {
        this(conn, null);
    }

    public Programmer(SerialInterface conn, String protocol) {

        this.conn = conn;
        this.protocol = protocol;
        if (Utils.inArray(this.protocol, PROTOSET_PARITY)) {
            this.conn.setParity(true);
        } else {
            this.conn.setParity(false);
        }
        this.chkmode = 0;
    }

    protected byte[] readConn(int size, long time) {
        long endTime = time + System.currentTimeMillis() + time;
        ByteBuffer byteBuffer = getByteBuffer();
        while (byteBuffer.position() < size) {
            byte[] src = conn.read(size - byteBuffer.position());
            byteBuffer.put(src);
            if (System.currentTimeMillis() > endTime) {
                break;
            }
            try {
                Thread.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return Utils.data(byteBuffer);
    }

    protected byte[] readConn(int size) {
        return readConn(size, 100000);
    }


    @SuppressLint("DefaultLocale")
    protected Object[] database(byte[] model) {
        byte[][] iapmcu = new byte[][]{
                {(byte) 0xD1, 0x3F},
                {(byte) 0xD1, 0x5F}, {(byte) 0xD1, (byte) 0x7F},
                {(byte) 0xD2, 0x7E}, {(byte) 0xD2, (byte) 0xFE},
                {(byte) 0xD3, 0x5F}, {(byte) 0xD3, (byte) 0xDF},
                {(byte) 0xE2, 0x76}, {(byte) 0xE2, (byte) 0xF6},

        };
        try {
            ModelMap modelMap = new ModelMap();

            ModelMap.Model modelBean = modelMap.getModelBy(model[0]);
            String prefix = modelBean.prefix;
            int romratio = modelBean.romratio;
            ModelMap.Node[] fixmap = modelBean.fixmap;

            if (in(model[0], (byte) 0xF0, (byte) 0xF1) && between(model[1], (byte) 0x20, (byte) 0x30)) {
                prefix = "90";
            }
            String infix = "", postfix = "";
            int romsize;
            String romfix;
            byte[] keys = new byte[0];


            for (ModelMap.Node node : fixmap) {
                keys = node.keys;
                if (between(model[1], keys[0], keys[1])) {
                    infix = node.names[0];
                    postfix = node.names[1];
                    break;
                }
            }

            romsize = romratio * (model[1] - keys[0]);

            if (model[0] == (byte) 0xF0 && model[1] == (byte) 0x03) {
                romsize = 13;
            }
            if (in(model[0], (byte) 0xf0, (byte) 0xf1)) {
                romfix = "" + (model[1] - keys[0]);
            } else if (model[0] == (byte) 0xF2) {
                romfix = "" + romsize;
            } else {
                romfix = String.format("%02d", romsize);
            }

            String name = Utils.inArrayArray(model, iapmcu) ? "IAP" : "STC";
            name += prefix + infix + romfix + postfix;
            return new Object[]{name, romsize};
        } catch (Exception e) {
            e.printStackTrace();
            return new Object[]{String.format("Unknown %02X %02X", model[0], model[1])};
        }

    }


    Result waitingResponse() {
        return waitingResponse(1000, new byte[]{0x46, (byte) 0xB9, 0x68});
    }

    Result waitingResponse(int timeout) {
        return waitingResponse(timeout, new byte[]{0x46, (byte) 0xB9, 0x68});
    }


    Result waitingResponse(double timeout, byte[] start) {

        byte[] readConn = readConn(start.length, (long) (timeout * 1000));
        if (!Arrays.equals(readConn, start)) {
            //System.out.println("read time out");
            return null;
        }

        int chksum = start[start.length - 1];
        byte[] bytes = readConn(2);
        int dataLength = Utils.getInt(bytes[0], bytes[1]);
        if (dataLength > 64) {
            System.out.println("recv(..): Incorrect packet size");
            return null;
        }


        chksum += Utils.sum(bytes);
        bytes = readConn(dataLength - 3);
        System.out.println("-->" + HexUtils.toString(bytes));
        if (bytes[bytes.length - 1] != 0x16) {
            System.out.println("recv(..): Missing terminal symbol");
            return null;
        }

        chksum += Utils.sum(Utils.sub(bytes, 0, -(1 + this.chkmode)));
        byte rec = Utils.low(chksum);
        if (chkmode > 0 && rec != bytes[bytes.length - 2]) {
            System.err.printf("recv(..): Incorrect checksum[0] (%02X != %02X ) \n", rec, bytes[bytes.length - 2]);
        } else if (chkmode > 1 && ((chksum >> 8) & 0xff) != bytes[bytes.length - 3]) {
            System.err.println("recv(..): Incorrect checksum[1]");
        }
        return new Result(bytes[0], Utils.sub(bytes, 1, -(1 + this.chkmode)));
    }


    ///////////////////
    void send(byte cmd, byte[] dat) {

        ByteBuffer buf = getByteBuffer();

        buf.put(new byte[]{0x46, (byte) 0xB9, 0x6A});
        int n = 1 + 2 + 1 + dat.length + this.chkmode + 1;
        buf.put(new byte[]{(byte) (n >> 8), (byte) (n & 0xff), cmd});
        buf.put(dat);

        int chksum = Utils.sum(Utils.sub(buf.array(), 2, buf.position()));
        if (chkmode > 1) {
            buf.put((byte) ((chksum >> 8) & 0xFF));
        }
        buf.put(new byte[]{(byte) (chksum & 0xFF), 0x16});

        byte[] data = Utils.data(buf);
        System.out.println("cmd: " + HexUtils.toString(data));
        try {
            conn.write(data);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    boolean startFlag = false;

    @SuppressLint("DefaultLocale")
    boolean detect() {
        byte[] send = new byte[]{0x7F};
        Result result = null;
        byte[] recv = new byte[]{0x68};
        while (result == null) {
            try {
                conn.write(send);
            } catch (IOException e) {
                e.printStackTrace();
                return false;
            }
            result = waitingResponse(0.05, recv);
        }


        startFlag = true;


        float sum = Utils.sum2(result.data, 16);

        this.fosc = sum * this.conn.getBaudrate() / 580974;
        this.info = Utils.sub(result.data, 16, result.data.length);
        this.version = String.format("%d.%d%c", this.info[0] >> 4, this.info[0] & 0x0F, this.info[1] & 0xff);
        this.model = Utils.sub(info, 3, 5);
        Object[] database = database(model);
        this.name = (String) database[0];
        this.romsize = (int) database[1];


        System.out.println("_______________________________");
        System.out.printf("Model ID: %02X %02X \n", model[0], model[1]);
        System.out.printf("Model name: %s \n", this.name);
        System.out.printf("ROM size: %d \n", this.romsize);
        if (this.protocol == null) {
            this.protocol = getProtocol(this.model[0]);
        }
        if (Utils.inArray(this.protocol, PROTOSET_PARITY)) {
            this.chkmode = 2;
            conn.setParity(true);
        } else {
            this.chkmode = 1;
            this.conn.setParity(false);
        }
        if (protocol != null) {
            info = Utils.sub(info, 0, -this.chkmode);
            System.out.printf("Protocol ID: %s \n", protocol);
            System.out.printf("Checksum mode: %d \n", chkmode);
            System.out.printf("UART Parity: UNKNOW \n");


        }
        return true;
    }

    private String getProtocol(byte b) {
        switch (b) {
            case (byte) 0xF0:
                return PROTOCOL_89;
            case (byte) 0xF1:
                return PROTOCOL_89;
            case (byte) 0xF2:
                return PROTOCOL_12Cx052;
            case (byte) 0xD1:
                return PROTOCOL_12C5A;
            case (byte) 0xD2:
                return PROTOCOL_12C5A;
            case (byte) 0xE1:
                return PROTOCOL_12C52;
            case (byte) 0xE2:
                return PROTOCOL_12C5A;
            case (byte) 0xE6:
                return PROTOCOL_12C52;
        }
        return null;
    }

    void print_info() {
        System.out.println("--------------------------------");
        System.out.printf(" FOSC: %.3f MHz \n", this.fosc);
        System.out.printf(" Model: %s (ver %s ) \n", this.name, this.version);
        programListener.onInfo("连接单片机:" + name);
        programListener.onInfo("Rom大小:" + romsize+" KB");
        if (romsize > 0) {
            System.out.printf(" ROM: %d KB \n", this.romsize);
        }
        Switch[] switches = null;
        if (protocol == PROTOCOL_89) {
            switches = new Switch[]{
                    new Switch(2, 0x80, "Reset stops watchdog"),
                    new Switch(2, 0x40, "Internal XRAM"),
                    new Switch(2, 0x20, "Normal ALE pin"),
                    new Switch(2, 0x10, "Full gain oscillator"),
                    new Switch(2, 0x08, "Not erase data EEPROM"),
                    new Switch(2, 0x04, "Download regardless of P1"),
                    new Switch(2, 0x01, "12T mode"),
            };
        } else if (protocol == PROTOCOL_12C5A) {
            switches = new Switch[]{
                    new Switch(6, 0x40, "Disable reset2 low level detect"),
                    new Switch(6, 0x01, "Reset pin not use as I/O port"),
                    new Switch(7, 0x80, "Disable long power-on-reset latency"),
                    new Switch(7, 0x40, "Oscillator high gain"),
                    new Switch(7, 0x02, "External system clock source"),
                    new Switch(8, 0x20, "WDT disable after power-on-reset"),
                    new Switch(8, 0x04, "WDT count in idle mode"),
                    new Switch(10, 0x02, "Not erase data EEPROM"),
                    new Switch(10, 0x01, "Download regardless of P1"),
            };

            System.out.printf(" WDT prescal: %d\n", (int) Math.pow(2, ((this.info[8] & 0x07) + 1)));

        } else if (Utils.inArray(protocol, PROTOSET_12B)) {
            switches = new Switch[]{
                    new Switch(8, 0x02, "Not erase data EEPROM"),
            };
        } else {
            switches = new Switch[0];
        }

        System.out.println("--------------------------------");

    }


    void handshake() {
        int oldBaud = this.conn.getBaudrate();
        int[] bauds = new int[]{115200, 57600, 38400, 28800, 19200, 14400, 9600, 4800, 2400, 1200};
        int currentBaud = bauds[0];

        ByteBuffer baudstr = ByteBuffer.allocate(256);
        for (int baud : bauds) {
            if (bluetoothMode) {
                baud = 9600;
            }
            currentBaud = baud;
            float t = this.fosc * 1000000 / baud / 32;
            int tcfg = 0;
            if (!Utils.inArray(protocol, PROTOSET_89)) {
                t *= 2;
            }
            if (Math.abs(Math.round(t) - t) / t > 0.03) {
                continue;
            }
            if (Utils.inArray(protocol, PROTOSET_89)) {
                tcfg = 0x10000 - (int) (t + 0.5);
            } else {
                if (t > 0xFF) {
                    continue;
                }
                tcfg = 0xC000 + 0x100 - (int) (t + 0.5);
            }
            baudstr.rewind();
            baudstr.put(new byte[]{
                    (byte) (tcfg >> 8),
                    (byte) (tcfg & 0xFF),
                    (byte) (0xFF - (tcfg >> 8)),
                    (byte) Math.min((256 - (tcfg & 0xFF)) * 2, 0xFE),
                    (byte) (oldBaud / 60)});


            int[] freqlist;

            if (Utils.inArray(protocol, PROTOSET_89)) {
                freqlist = new int[]{40, 20, 10, 5};
            } else {
                freqlist = new int[]{30, 24, 20, 12, 6, 3, 2, 1};

            }
            int twait;
            for (twait = 0; twait < freqlist.length; twait++) {
                if (this.fosc > freqlist[twait]) {
                    break;
                }
            }
            System.out.printf("Waiting time config %02X \n", (0x80 + twait));
            baudstr.put((byte) (0x80 + twait));
            this.send((byte) 0x8F, Utils.data(baudstr));


            Utils.sleep(0.1);
            conn.setBaudrate(baud);


            if (waitingResponse(2) == null) {
                System.out.printf("Cannot use baudrate %d \n", baud);
                conn.setBaudrate(oldBaud);
            } else {
                conn.setBaudrate(oldBaud);
                break;
            }
        }
        System.out.printf("Change baudrate to %d \n", currentBaud);
        send((byte) 0x8E, Utils.data(baudstr));
        Utils.sleep(0.1);
        conn.setBaudrate(currentBaud);

        waitingResponse();

    }

    void erase() {
        if (Utils.inArray(protocol, PROTOSET_89)) {
            send((byte) 0x84, new byte[]{0x01, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33});
            Result recv = waitingResponse();
            assert recv.cmd == (byte) 0x80;

        } else {
            ByteBuffer byteBuffer = getByteBuffer();
            byteBuffer.put(new byte[]{0x00, 0x00, (byte) (this.romsize * 4)});
            byteBuffer.put(new byte[]{0x00, 0x00, (byte) (this.romsize * 4)});
            byteBuffer.put(Utils.create(12, (byte) 0x00));
            for (int i = 0x80; i >= 0x0d - 1; i--) {
                byteBuffer.put((byte) i);
            }
            send((byte) 0x84, Utils.data(byteBuffer));
            waitingResponse();
        }


    }

    void flash(byte[] code) {
        ByteBuffer byteBuffer = getByteBuffer();
        byteBuffer.put(code);
        byteBuffer.put(Utils.create((511 - (code.length - 1) % 512), (byte) 0xff));
        code = Utils.data(byteBuffer);


        for (int i = 0; i < code.length; i += 128) {
            System.out.printf("Flash code region (%04X, %04X)\n", i, i + 127);
            byte[] addr = new byte[]{0, 0, (byte) (i >> 8), (byte) (i & 0xFF), 0, (byte) 128};
            byte[] sub = Utils.sub(code, i, i + 128);
            byteBuffer = getByteBuffer();
            byteBuffer.put(addr).put(sub);
            send((byte) 0x00, Utils.data(byteBuffer));
            Result recv = waitingResponse();
//            assert recv.data[0] == Utils.sum(sub) % 256;
            System.out.printf("progress=%.2f \n", (i + 128f) / code.length);
            programListener.onInfo(String.format("下载进度:%d%%", (int)(100f*(i + 128f) / code.length)));
        }
    }

    boolean options(boolean erase_eeprom) {
        byte[] fosc1 = new byte[]{(byte) (this.fosc * 1000000)};
        ByteBuffer dat = getByteBuffer();

        if (protocol == PROTOCOL_89) {
            if (erase_eeprom) {
                this.info[2] &= 0xF7;
                this.info[2] |= (erase_eeprom ? 0x00 : 0x08);
            }
            dat.put(Utils.sub(info, 2, 3));
            dat.put(new byte[]{(byte) 0xff, (byte) 0xff, (byte) 0xff});

        } else if (protocol == PROTOCOL_12C5A) {
            if (erase_eeprom) {
                this.info[10] &= 0xFD;
                this.info[10] |= (erase_eeprom ? 0x00 : 0x02);
            }
            dat.put(Utils.sub(info, 6, 9))
                    .put(Utils.create(5, (byte) 0xff))
                    .put(Utils.sub(info, 10, 11))
                    .put(Utils.create(6, (byte) 0xff))
                    .put(fosc1);


        } else if (Utils.inArray(protocol, PROTOSET_12B)) {
            if (erase_eeprom) {
                this.info[8] &= 0xFD;
                this.info[8] |= (erase_eeprom ? 0x00 : 0x02);
                dat.put(Utils.sub(info, 6, 11))
                        .put(fosc1)
                        .put(Utils.sub(info, 12, 16))
                        .put(Utils.create(4, (byte) 0xff))
                        .put(Utils.sub(info, 8, 9))
                        .put(Utils.create(7, (byte) 0xff))
                        .put(fosc1)
                        .put(Utils.create(3, (byte) 0xff))
                ;
            }


        } else if (Utils.inArray(protocol, PROTOSET_12B)) {
            if (erase_eeprom) {
                System.out.println("Modifying options is not supported for this target");
                return false;
            }

        }
        if (dat.position() > 0) {
            send((byte) 0x8d, Utils.data(dat));
            waitingResponse();
        }

        return true;
    }

    private void terminate() {
        System.out.println("Send termination command");
        this.send((byte) 0x82, new byte[0]);

        Utils.sleep(0.2);
    }

    private void baudCheck() {
        if (Utils.inArray(protocol, PROTOSET_PARITY)) {
            System.out.println("baudCheck -->0x50");
            send((byte) 0x50, Utils.merg(new byte[]{0x00, 0x00, 0x36, 0x01}, this.model));
            Result recv = waitingResponse();
            assert recv.cmd == (byte) 0x8f && recv.data.length == 0;
        }
    }

    private void confirm() {

        if (!Utils.inArray(protocol, PROTOSET_PARITY)) {
            for (int i = 0; i < 5; i++) {
                send((byte) 0x80, Utils.merg(new byte[]{0x00, 0x00, 0x36, 0x01}, this.model));
                Result recv = waitingResponse();
                assert recv.cmd == (byte) 0x80 && recv.data.length == 0;
            }

        }
    }

    private void unknown_packet_3() {
        if (Utils.inArray(protocol, PROTOSET_PARITY)) {
            System.out.println("Send unknown packet (50 00 00 36 01 ...)");
            send((byte) 0x69, Utils.merg(new byte[]{0x00, 0x00, 0x36, 0x01}, this.model));
            Result recv = waitingResponse();
            assert recv.cmd == (byte) 0x8d && recv.data.length == 0;
        }
    }

    void autoisp(SerialInterface conn, int baud, String magic) {
        if (magic == null) {
            return;
        }
        int bak = conn.getBaudrate();
        conn.setBaudrate(baud);
        try {
            conn.write(magic.getBytes(Charset.defaultCharset()));
        } catch (IOException e) {
            e.printStackTrace();
        }
        Utils.sleep(0.5);
        conn.setBaudrate(bak);
    }

    public boolean program(byte[] code) {//null
        System.out.println("等待连接单片机...");
        programListener.onInfo("等待连接单片机");
        if (!detect()) {
            System.out.println("failed");
            programListener.onInfo("连接失败");
            return false;
        }

        print_info();
        if (protocol == null) {
            System.err.println("Unsupported target");
            programListener.onInfo("不支持的单片机");
            return false;
        }
        if (code == null) {
            return false;
        }
        programListener.onInfo("测试波特率");
        System.out.println("baudCheck...");
        baudCheck();

        handshake();
        System.out.println("Baudrate: " + conn.getBaudrate());
        programListener.onInfo("波特率:" + conn.getBaudrate());
        confirm();

        System.out.println("Erasing target...");
        programListener.onInfo("准备下载");
        erase();

        System.out.printf("Size of the binary: %d\n", code.length);
        System.out.println("Programming");
        programListener.onInfo(String.format("下载文件大小:%d ", code.length));
        programListener.onInfo("下载中...");
        flash(code);
        System.out.println("done");
        unknown_packet_3();
        System.out.println("Setting options...");

        if (options(false)) {
            System.out.println("done");

        } else {
            System.out.println("failed");
        }
        terminate();
        programListener.onInfo("完成");
        return true;
    }

    public boolean downloadBin(String path) {
        byte[] data = FUtils.getFixBin(path);

        System.out.println("data len:" + data.length);
        return program(data);
    }

    public boolean downloadHex(String path) {
        byte[] data = FUtils.file2byte(Hex2Bin.conv(path).getAbsolutePath());
        return program(data);

    }


    class Result {
        byte cmd;
        byte[] data;

        public Result(byte cmd, byte[] data) {
            this.cmd = cmd;
            this.data = data;
        }
    }

    class Switch {
        int pos;
        int bit;
        String desc;

        public Switch(int pos, int bit, String desc) {
            this.pos = pos;
            this.bit = bit;
            this.desc = desc;
        }
    }


   public interface ProgramListener {
        void onInfo(String info);
    }
}
